﻿// Copyright 2012 Stuart Turner
// Licensed under the Apache License, Version 2.0 [http://www.apache.org/licenses/LICENSE-2.0]

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;

using Complete.Data.Support;

namespace Complete.Data.Linq.Expressions
{
	public abstract class SourceExpression : CompleteExpression
	{
		internal SourceExpression(CompleteExpressionType eType, Type type)
			: base(eType, type)
		{
		}

		protected List<ColumnExpression> _Columns;
		public IList<ColumnExpression> Columns { get { return _Columns.AsReadOnly(); } }
		protected List<AssignmentInformation> _Assignments;
		public IList<AssignmentInformation> Assignments { get { return _Assignments.AsReadOnly(); } }

		internal void AddAssignment(AssignmentInformation assign)
		{
			_Assignments.Add(assign);
		}
	}

	public abstract partial class TableValueExpression : SourceExpression
	{
		internal TableValueExpression(CompleteExpressionType eType, Type type)
			: base(eType, type)
		{
		}

		public string Alias { get; protected set; }
		public LambdaExpression Projection { get; protected set; }
		public bool IsProjection { get; protected set; }
	}

	public class SelectExpression : TableValueExpression
	{
		internal SelectExpression(LambdaExpression projection, FromExpression from, WhereExpression where, string alias, bool isProjection, Expression take, OrderByClause orderBy)
			: base(CompleteExpressionType.Select, typeof(IEnumerable<>).MakeGenericType(projection.ReturnType))
		{
			this.Projection = projection;
			this.Alias = alias;
			this.From = from;
			this.Where = where;
			this.Take = take;
			this.OrderBy = orderBy;
			this.IsProjection = isProjection;

			var ce = ColumnExtractor.ExtractColumns(this, projection);
			this._Columns = ce.Columns.ToList();
			this._Assignments = ce.Assignments.ToList();
		}

		public ColumnExpression AddUnmappedColumn(Expression column)
		{
			var colName = string.Format("x{0:hhMMssffffff}", DateTime.UtcNow);
			while (_Columns.Any(x => x.Name == colName))
				colName = string.Format("x{0:hhMMssffffff}", DateTime.UtcNow);

			var colExpr = CompleteExpression.Column(column, colName, this.Alias);
			_Columns.Add(colExpr);

			return colExpr;
		}

		public ColumnExpression TryFindColumn(Expression column)
		{
			return null;
		}

		public new FromExpression From { get; internal set; }
		public new WhereExpression Where { get; internal set; }
		public Expression Take { get; internal set; }
		public OrderByClause OrderBy { get; internal set; }

		internal bool DefaultIfEmpty { get; set; }
	}

	public class ColumnExpression : CompleteExpression
	{
		internal ColumnExpression(string alias, string name, int ordinal, Expression declaration)
			: base(CompleteExpressionType.Column, declaration.Type)
		{
			this.Alias = alias;
			this.Name = name;
			this.Ordinal = ordinal;
			this.Declaration = declaration;
		}

		public ColumnExpression Clone()
		{
			return new ColumnExpression(
				this.Alias,
				this.Name,
				this.Ordinal,
				this.Declaration);
		}

		public string Alias { get; internal set; }
		public string Name { get; internal set; }
		public int Ordinal { get; internal set; }
		public Expression Declaration { get; private set; }

		internal Expression MembershipPath { get; set; }
	}

	public class TableExpression : TableValueExpression
	{
		internal TableExpression(Type type, string alias, string name, LambdaExpression projection)
			: base(CompleteExpressionType.Table, type)
		{
			this.Alias = alias;
			this.Name = name;
			this.Projection = projection;
			this.IsProjection = false;

			var ce = ColumnExtractor.ExtractColumns(this, projection);
			this._Columns = ce.Columns.ToList();
			this._Assignments = ce.Assignments.ToList();
		}

		public string Name { get; private set; }
	}

	public partial class FromExpression : CompleteExpression
	{
		internal FromExpression(SourceExpression from)
			: base(CompleteExpressionType.From, from.Type)
		{
			this.From = from;
		}

		public new SourceExpression From { get; private set; }
		internal void UpdateSource(SourceExpression exp)
		{
			this.From = exp;
		}

		internal ColumnExpression FindColumn(MemberExpression exp)
		{
			foreach (var col in From.Columns)
				if (AreEqual(col.MembershipPath, exp))
					return col;
			return null;
		}

		internal Expression FindObject(Expression exp)
		{
			foreach (var ass in From.Assignments)
				if (AreEqual(ass.MembershipPath, exp))
					return ass.Declaration;
			return null;
		}

		private bool AreEqual(Expression expr1, Expression expr2)
		{
			if (expr1 == expr2) return true;
			if (expr1.NodeType != expr2.NodeType) return false;

			switch (expr1.NodeType)
			{
				case ExpressionType.Parameter:
					return expr1.Type == expr2.Type;

				case ExpressionType.MemberAccess:
					return AreEqual(expr1 as MemberExpression, expr2 as MemberExpression);

				default:
					throw new NotSupportedException("What happened here?");
			}
		}

		private bool AreEqual(MemberExpression me1, MemberExpression me2)
		{
			if (me1 == me2) return true;
			if (me1.Member != me2.Member) return false;

			switch (me1.Expression.NodeType)
			{
				case ExpressionType.Parameter:
					switch (me2.Expression.NodeType)
					{
						case ExpressionType.Parameter:
							return true;
						case ExpressionType.MemberAccess:
							return AreEqual((MemberExpression)me2.Expression, (ParameterExpression)me1.Expression);
						default:
							throw new NotSupportedException("What happened here?");
					}

				case ExpressionType.MemberAccess:
					switch (me2.Expression.NodeType)
					{
						case ExpressionType.Parameter:
							return AreEqual((MemberExpression)me1.Expression, (ParameterExpression)me2.Expression);
						case ExpressionType.MemberAccess:
							return AreEqual((MemberExpression)me1.Expression, (MemberExpression)me2.Expression);
						default:
							throw new NotSupportedException("What happened here?");
					}

				default:
					throw new NotSupportedException("What happened here?");
			}
		}

		private bool AreEqual(MemberExpression me1, ParameterExpression p2)
		{
			return this.FindObject(me1) != null;
		}
	}

	public class JoinExpression : SourceExpression
	{
		internal JoinExpression(SourceExpression left, SourceExpression right, JoinType joinType, Expression comparison)
			: base(CompleteExpressionType.Join, null)
		{
			this.Left = left;
			this.Right = right;
			this.JoinType = joinType;
			this.Comparison = comparison;

			base._Columns = left.Columns.Union(right.Columns).ToList();
			base._Assignments = left.Assignments.Union(right.Assignments).ToList();
		}

		public SourceExpression Left { get; private set; }
		public SourceExpression Right { get; private set; }
		public JoinType JoinType { get; private set; }
		public Expression Comparison { get; private set; }
	}

	public class WhereExpression : CompleteExpression
	{
		internal WhereExpression(Expression predicate, WhereExpression previousWhere)
			: base(CompleteExpressionType.Where, typeof(bool))
		{
			if (predicate == null) throw new ArgumentNullException("predicate");
			if (predicate.Type != typeof(bool))
				throw new ArgumentException("'predicate' must return a bool.", "predicate");

			if (previousWhere != null)
			{
				predicate =
					Expression.AndAlso(
						predicate,
						previousWhere.Predicate);
			}

			this.Predicate = predicate;
		}

		public Expression Predicate { get; private set; }
	}

	public class OrderByClause
	{
		internal OrderByClause(OrderByClause secondarySorts)
		{
			_SortColumns = new List<Tuple<Expression, SortDirection>>();
			SecondarySortingClause = secondarySorts;
		}

		private List<Tuple<Expression, SortDirection>> _SortColumns;
		public ReadOnlyCollection<Tuple<Expression, SortDirection>> SortColumns { get { return _SortColumns.AsReadOnly(); } }

		public void AppendColumn(Tuple<Expression, SortDirection> column)
		{
			_SortColumns.Add(column);
		}

		public void ClearColumns() { _SortColumns.Clear(); }

		public OrderByClause SecondarySortingClause { get; private set; }

		internal void InjectSecondarySortingClause(OrderByClause right)
		{
			var parent = this;
			while (parent.SecondarySortingClause != null)
				parent = parent.SecondarySortingClause;
			parent.SecondarySortingClause = right;
		}

		internal static OrderByClause CombineOrderByClauses(OrderByClause left, OrderByClause right)
		{
			// if left is null, then take right (null or no) and we're done
			if (left == null)
				return right;

			// left is not null, start with clone of left
			var orderBy = new OrderByClause(left.SecondarySortingClause);
			orderBy._SortColumns.AddRange(left._SortColumns);

			// if right is null, then we're done
			if (right != null)
			{
				// right is not null; merge in sort columns from right at top level
				left._SortColumns.AddRange(right._SortColumns);

				// start with the highest level orderBy to figure out where to inject the right side sorting clause
				// don't care about destroying existing clauses: these are cloned at every level anyway
				// these order by clauses exist only for current level
				var parent = orderBy;
				while (parent.SecondarySortingClause != null)
					parent = parent.SecondarySortingClause;
				parent.SecondarySortingClause = right.SecondarySortingClause;
			}

			return orderBy;
		}

		public enum SortDirection
		{
			Ascending,
			Descending,
		}
	}
}
